Enable/disable the okay button based on the presence of text in the To: field for...
[adiumx.git] / Plugins / Contact Status Dock Overlays / AIContactStatusDockOverlaysPlugin.m
blobceeae817b5a645eed1813a70f905b256148a382a
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "AIContactStatusDockOverlaysPlugin.h"
18 #import <Adium/AIChatControllerProtocol.h>
19 #import <Adium/AIContactControllerProtocol.h>
20 #import <Adium/AIContentControllerProtocol.h>
21 #import "AIDockController.h"
22 #import <Adium/AIInterfaceControllerProtocol.h>
23 #import <Adium/AIPreferenceControllerProtocol.h>
24 #import <Adium/AIContactAlertsControllerProtocol.h>
25 #import <AIUtilities/AIColorAdditions.h>
26 #import <AIUtilities/AIDictionaryAdditions.h>
27 #import <AIUtilities/AIParagraphStyleAdditions.h>
28 #import <AIUtilities/AIArrayAdditions.h>
29 #import <AIUtilities/AIImageAdditions.h>
30 #import <Adium/AIAbstractListController.h>
31 #import <Adium/AIAccount.h>
32 #import <Adium/AIChat.h>
33 #import <Adium/AIIconState.h>
35 #define SMALLESTRADIUS                          15
36 #define RADIUSRANGE                                     36
37 #define SMALLESTFONTSIZE                        14
38 #define FONTSIZERANGE                           30
40 #define DOCK_OVERLAY_ALERT_SHORT        AILocalizedString(@"Display name in the dock icon",nil)
41 #define DOCK_OVERLAY_ALERT_LONG         DOCK_OVERLAY_ALERT_SHORT
43 @interface AIContactStatusDockOverlaysPlugin (PRIVATE)
44 - (void)_setOverlay;
45 - (NSImage *)overlayImageFlash:(BOOL)flash;
46 - (void)preferencesChanged:(NSNotification *)notification;
47 - (void)flushPreferenceColorCache;
48 @end
50 @implementation AIContactStatusDockOverlaysPlugin
52 /*!
53 * @brief Install
54  */
55 - (void)installPlugin
57         overlayObjectsArray = [[NSMutableArray alloc] init];
58     overlayState = nil;
60     //Register as a contact observer (For signed on / signed off)
61     [[adium contactController] registerListObjectObserver:self];
62         
63         //Register as a chat observer (for unviewed content)
64         [[adium chatController] registerChatObserver:self];
65         
66         [[adium notificationCenter] addObserver:self
67                                                                    selector:@selector(chatClosed:)
68                                                                            name:Chat_WillClose
69                                                                          object:nil];
70         
71     //Prefs
72         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_LIST_THEME];
73         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
74         
75     //
76     image1 = [[NSImage alloc] initWithSize:NSMakeSize(128,128)];
77     image2 = [[NSImage alloc] initWithSize:NSMakeSize(128,128)];
78         
79         //Install our contact alert
80         [[adium contactAlertsController] registerActionID:DOCK_OVERLAY_ALERT_IDENTIFIER withHandler:self];
83 - (void)uninstallPlugin
85         [[adium contactController] unregisterListObjectObserver:self];
86         [[adium chatController] unregisterChatObserver:self];
87         [[adium notificationCenter] removeObserver:self];
88         [[adium preferenceController] unregisterPreferenceObserver:self];
91 /*!
92 * @brief Short description
93  * @result A short localized description of the action
94  */
95 - (NSString *)shortDescriptionForActionID:(NSString *)actionID
97         return DOCK_OVERLAY_ALERT_SHORT;
101 * @brief Long description
102  * @result A longer localized description of the action which should take into account the details dictionary as appropraite.
103  */
104 - (NSString *)longDescriptionForActionID:(NSString *)actionID withDetails:(NSDictionary *)details
106         return DOCK_OVERLAY_ALERT_LONG;
110 * @brief Image
111  */
112 - (NSImage *)imageForActionID:(NSString *)actionID
114         //XXX
115         return [NSImage imageNamed:@"DockAlert" forClass:[self class]];
119  * @brief Details pane
120  * @result An <tt>AIModularPane</tt> to use for configuring this action, or nil if no configuration is possible.
121  */
122 - (AIModularPane *)detailsPaneForActionID:(NSString *)actionID
124         return nil;
128 * @brief Perform an action
130  * @param actionID The ID of the action to perform
131  * @param listObject The listObject associated with the event triggering the action. It may be nil
132  * @param details If set by the details pane when the action was created, the details dictionary for this particular action
133  * @param eventID The eventID which triggered this action
134  * @param userInfo Additional information associated with the event; userInfo's type will vary with the actionID.
135  */
136 - (BOOL)performActionID:(NSString *)actionID forListObject:(AIListObject *)listObject withDetails:(NSDictionary *)details triggeringEventID:(NSString *)eventID userInfo:(id)userInfo
138         BOOL isMessageEvent = [[adium contactAlertsController] isMessageEvent:eventID];
139         
140         if (isMessageEvent) {
141                 AIChat  *chat;
143                 if ((chat = [userInfo objectForKey:@"AIChat"]) &&
144                    (chat != [[adium interfaceController] activeChat]) &&
145                    (![overlayObjectsArray containsObjectIdenticalTo:chat])) {
146                         [overlayObjectsArray addObject:chat];
147                         
148                         //Wait until the next run loop so this event is done processing (and our unviewed content count is right)
149                         [self performSelector:@selector(_setOverlay)
150                                            withObject:nil
151                                            afterDelay:0];
153                         /* The chat observer method is responsible for removing this overlay later */
154                 }
156         } else if (listObject) {
157                 NSTimer *removeTimer;
158                 
159                 //Clear any current timer for this object o ahve its overlay removed
160                 if ((removeTimer = [listObject statusObjectForKey:@"DockOverlayRemoveTimer"])) [removeTimer invalidate];
161                 
162                 //Add a timer to remove this overlay
163                 removeTimer = [NSTimer scheduledTimerWithTimeInterval:5
164                                                                                                            target:self
165                                                                                                          selector:@selector(removeDockOverlay:)
166                                                                                                          userInfo:listObject
167                                                                                                           repeats:NO];
168                 [listObject setStatusObject:removeTimer
169                                                          forKey:@"DockOverlayRemoveTimer"
170                                                          notify:NotifyNever];
172                 if (![overlayObjectsArray containsObject:listObject]) {
173                         [overlayObjectsArray addObject:listObject];
174                 }
176                 //Wait until the next run loop so this event is done processing
177                 [self performSelector:@selector(_setOverlay)
178                                    withObject:nil
179                                    afterDelay:0];
180         }
181         
182         return YES;
185 - (void)removeDockOverlay:(NSTimer *)removeTimer
187         AIListObject    *inObject = [removeTimer userInfo];
189         [overlayObjectsArray removeObjectIdenticalTo:inObject];
190         
191         [inObject setStatusObject:nil
192                                            forKey:@"DockOverlayRemoveTimer"
193                                            notify:NotifyNever];
194         
195         [self _setOverlay];
198 - (void)chatClosed:(NSNotification *)notification
200         AIChat  *chat = [notification object];
201         
202         [overlayObjectsArray removeObjectIdenticalTo:chat];
203         
204         [self _setOverlay];
208 * @brief Allow multiple actions?
210  * If this method returns YES, every one of this action associated with the triggering event will be executed.
211  * If this method returns NO, only the first will be.
213  * Don't allow multiple dock actions to occur.  While a series of "Bounce every 5 seconds," "Bounce every 10 seconds,"
214  * and so on actions could be combined sanely, a series of "Bounce once" would make the dock go crazy.
215  */
216 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID
218         return NO;
221 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
222                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
223 {       
224         if ([group isEqualToString:PREF_GROUP_LIST_THEME]) {
225                 //Grab colors from status coloring plugin's prefs    
226                 [self flushPreferenceColorCache];
227                 signedOffColor = [[[prefDict objectForKey:KEY_SIGNED_OFF_COLOR] representedColor] retain];
228                 signedOnColor = [[[prefDict objectForKey:KEY_SIGNED_ON_COLOR] representedColor] retain];
229                 unviewedContentColor = [[[prefDict objectForKey:KEY_UNVIEWED_COLOR] representedColor] retain];
230                 
231                 backSignedOffColor = [[[prefDict objectForKey:KEY_LABEL_SIGNED_OFF_COLOR] representedColor] retain];
232                 backSignedOnColor = [[[prefDict objectForKey:KEY_LABEL_SIGNED_ON_COLOR] representedColor] retain];
233                 backUnviewedContentColor = [[[prefDict objectForKey:KEY_LABEL_UNVIEWED_COLOR] representedColor] retain];
235         } else if ([group isEqualToString:PREF_GROUP_APPEARANCE]) {
236                 if (!key || [key isEqualToString:KEY_ANIMATE_DOCK_ICON]) {
237                         BOOL newShouldAnimate = [[prefDict objectForKey:KEY_ANIMATE_DOCK_ICON] boolValue];
238                         if (newShouldAnimate != shouldAnimate) {
239                                 shouldAnimate = newShouldAnimate;
241                                 //Redo our overlay to respect our new preference
242                                 if (!firstTime) [self _setOverlay];
243                         }
244                 }
245         }
249 - (void)flushPreferenceColorCache
251         [signedOffColor release]; signedOffColor = nil;
252         [signedOnColor release]; signedOnColor = nil;
253         [unviewedContentColor release]; unviewedContentColor = nil;
254         [backSignedOffColor release]; backSignedOffColor = nil;
255         [backSignedOnColor release]; backSignedOnColor = nil;
256         [backUnviewedContentColor release]; backUnviewedContentColor = nil;     
259 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
261         if ([inObject isKindOfClass:[AIAccount class]]) {
262                 //When an account signs on or off, force an overlay update as it may have silently changed
263                 //contacts' statuses
264                 if ([inModifiedKeys containsObject:@"Online"]) {
265                         NSEnumerator    *enumerator = [[[overlayObjectsArray copy] autorelease] objectEnumerator];
266                         AIListObject    *listObject;
267                         BOOL                    madeChanges = NO;
268                         
269                         while ((listObject = [enumerator nextObject])) {
270                                 if (([listObject respondsToSelector:@selector(account)]) &&
271                                    ([(id)listObject account] == inObject) &&
272                                    ([overlayObjectsArray containsObjectIdenticalTo:listObject])) {
273                                         [overlayObjectsArray removeObject:listObject];
274                                         madeChanges = YES;
275                                 }
276                         }
277                         
278                         if (madeChanges) [self _setOverlay];
279                 }
280         }
281         
282         return nil;
286  * @brief When a chat no longer has unviewed content, remove it from display
287  */
288 - (NSSet *)updateChat:(AIChat *)inChat keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
290         if (inModifiedKeys == nil || [inModifiedKeys containsObject:KEY_UNVIEWED_CONTENT]) {
291                 
292                 if (![inChat unviewedContentCount]) {
293                         if ([overlayObjectsArray containsObjectIdenticalTo:inChat]) {
294                                 [overlayObjectsArray removeObjectIdenticalTo:inChat];
295                                 [self _setOverlay];
296                         }
297                 }
298         }
299         
300         return nil;
304 - (void)_setOverlay
306     //Remove & release the current overlay state
307     if (overlayState) {
308         [[adium dockController] removeIconStateNamed:@"ContactStatusOverlay"];
309         [overlayState release]; overlayState = nil;
310     }
312     //Create & set the new overlay state
313     if ([overlayObjectsArray count] != 0) {
314         //Set the state
315                 if (shouldAnimate) {
316                         overlayState = [[AIIconState alloc] initWithImages:[NSArray arrayWithObjects:[self overlayImageFlash:NO], [self overlayImageFlash:YES], nil]
317                                                                                                                  delay:0.5
318                                                                                                        looping:YES 
319                                                                                                            overlay:YES];
320                 } else {
321                         overlayState = [[AIIconState alloc] initWithImage:[self overlayImageFlash:NO]
322                                                                                                           overlay:YES];
323                 }
325         [[adium dockController] setIconState:overlayState named:@"ContactStatusOverlay"];
326     }   
330 - (NSImage *)overlayImageFlash:(BOOL)flash
332     NSEnumerator                *enumerator;
333     ESObjectWithStatus  *object;
334     NSFont                              *font;
335     NSParagraphStyle    *paragraphStyle;
336     float                               dockIconScale;
337     int                                 iconHeight;
338     float                               top, bottom;
339     NSImage                             *image = (flash ? image1 : image2);
340         
341     //Pre-calc some sizes
342     dockIconScale = 1- [[adium dockController] dockIconScale];
343     iconHeight = (SMALLESTRADIUS + (RADIUSRANGE * dockIconScale));
345         top = 126;
346         bottom = top - iconHeight;
348     //Set up the string details
349     font = [NSFont boldSystemFontOfSize:(SMALLESTFONTSIZE + (FONTSIZERANGE * dockIconScale))];
350     paragraphStyle = [NSParagraphStyle styleWithAlignment:NSCenterTextAlignment lineBreakMode:NSLineBreakByClipping];
351         
352     //Clear our image
353     [image lockFocus];
354     [[NSColor clearColor] set];
355     NSRectFillUsingOperation(NSMakeRect(0, 0, 128, 128), NSCompositeCopy);
356         
357     //Draw overlays for each contact
358     enumerator = [overlayObjectsArray reverseObjectEnumerator];
359     while ((object = [enumerator nextObject]) && !(top < 0) && bottom < 128) {
360         float                   left, right, arcRadius, stringInset;
361         NSBezierPath    *path;
362         NSColor                 *backColor = nil, *textColor = nil, *borderColor = nil;
363                 
364         //Create the pill frame
365         arcRadius = (iconHeight / 2.0f);
366         stringInset = (iconHeight / 4.0f);
367         left = 1 + arcRadius;
368         right = 127 - arcRadius;
369                 
370         path = [NSBezierPath bezierPath];
371         [path setLineWidth:((iconHeight/2.0) * 0.13333f)];
372         //Top
373         [path moveToPoint: NSMakePoint(left, top)];
374         [path lineToPoint: NSMakePoint(right, top)];
375                 
376         //Right rounded cap
377         [path appendBezierPathWithArcWithCenter:NSMakePoint(right, top - arcRadius) 
378                                                                                  radius:arcRadius
379                                                                          startAngle:90
380                                                                            endAngle:0
381                                                                           clockwise:YES];
382         [path lineToPoint: NSMakePoint(right + arcRadius, bottom + arcRadius)];
383         [path appendBezierPathWithArcWithCenter:NSMakePoint(right, bottom + arcRadius) 
384                                                                                  radius:arcRadius
385                                                                          startAngle:0
386                                                                            endAngle:270
387                                                                           clockwise:YES];
388                 
389         //Bottom
390         [path moveToPoint: NSMakePoint(right, bottom)];
391         [path lineToPoint: NSMakePoint(left, bottom)];
392                 
393         //Left rounded cap
394         [path appendBezierPathWithArcWithCenter:NSMakePoint(left, bottom + arcRadius)
395                                                                                  radius:arcRadius
396                                                                          startAngle:270
397                                                                            endAngle:180
398                                                                           clockwise:YES];
399         [path lineToPoint: NSMakePoint(left - arcRadius, top - arcRadius)];
400         [path appendBezierPathWithArcWithCenter:NSMakePoint(left, top - arcRadius) radius:arcRadius startAngle:180 endAngle:90 clockwise:YES];
401                 
402                 /*
403                  //Get our colors
404                  if (!([contact unviewedContentCount] && flash)) {
405                          backColor = [[contact displayArrayForKey:@"Label Color"] averageColor];
406                          textColor = [[contact displayArrayForKey:@"Text Color"] averageColor];
407                  }
408                  */
409                 
410         if ([object statusObjectForKey:KEY_UNVIEWED_CONTENT]) { //Unviewed
411                         if (flash) {
412                 backColor = [NSColor whiteColor];
413                 textColor = [NSColor blackColor];
414             } else {
415                 backColor = backUnviewedContentColor;
416                 textColor = unviewedContentColor;
417             }
418         } else if ([object integerStatusObjectForKey:@"Signed On"]) { //Signed on
419             backColor = backSignedOnColor;
420             textColor = signedOnColor;
421                         
422         } else if ([object integerStatusObjectForKey:@"Signed Off"]) { //Signed off
423             backColor = backSignedOffColor;
424             textColor = signedOffColor;
425                         
426         }
427                 
428                 if (!backColor) {
429                         backColor = [NSColor whiteColor];
430                 }
431                 if (!textColor) {
432                         textColor = [NSColor blackColor];
433                 }
434                 
435         //Lighten/Darken the back color slightly
436         if ([backColor colorIsDark]) {
437             backColor = [backColor darkenBy:-0.15];
438             borderColor = [backColor darkenBy:-0.3];
439         } else {
440             backColor = [backColor darkenBy:0.15];
441             borderColor = [backColor darkenBy:0.3];
442         }
443                 
444         //Draw
445         [backColor set];
446         [path fill];
447         [borderColor set];
448         [path stroke];
449                 
450         //Get the object's display name
451         [[object displayName] drawInRect:NSMakeRect(0 + stringInset, bottom + 1, 128 - (stringInset * 2), top - bottom)
452                            withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, textColor, NSForegroundColorAttributeName, nil]];
453                 /*        
454                         nameString = [[[NSAttributedString alloc] initWithString:[contact displayName] attributes:[NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, textColor, NSForegroundColorAttributeName, nil]] autorelease];
455         [nameString drawInRect:NSMakeRect(0 + stringInset, bottom + 1, 128 - (stringInset * 2), top - bottom)];*/
456                 
457         //Move down to the next pill
458                 top -= (iconHeight + 7.0 * dockIconScale);
459                 bottom = top - iconHeight;
460     }
461         
462     [image unlockFocus];
463     
464     return image;
467 @end